/* 
 * Copyright (C) 2014 Krzysztof Troska <elleander86 at gmail.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package anidbjclient;

import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * Daemon which gets counted ed2k sums from database and calls processNewFile on them
 * @author Krzysztof Troska <elleander86 at gmail.com>
 */
public class AnidbInserter implements Runnable {
    private final Database DB = AnidbJClientGUI.DB;
    private final ConfigManager cm = new ConfigManager();
    private static final int INTERVAL = 30;
    private static final String[] columns = {"ed2k", "path", "fid"};
    private static final String database = "files_to_add";
    
    public static volatile List<Thread> threads = new ArrayList<>();
    private static boolean canceled = false; 
    
    /**
     * Stops function in safe way
     */
    public static void cancel() { 
        canceled = true; 
        FileWalker.canceled = true; 
    }
    
    /**
     * Runs in background and in set intervals sec intervals check database for new files 
     * to add then sleeps again.
     * Can be killed by using {@link #cancel() cancel()}
     */
    @Override
    public void run() {
        for (Thread thread : threads) {
            if(thread.isAlive()) {
                System.err.println("One daemon is still alive, can't start another one");
                thread.interrupt(); // interupt it so it starts again if needed.
                return; // don't start another thread if one is alive.
            }
        }
        threads.add(Thread.currentThread());
        /**
         * We want to delete all files which did not get ed2k somehow, 
         * so we can count them again later.
         */
        DB.deleteEmptyFromFilesToAdd(); 
        while(!canceled) {
            System.out.println("Inserter started...");
            /**
             * Starts the file walker which starts ed2k counting hash for files
             */
            this.startFileWalker();
            if(canceled) // we want to cancel BEFORE checking DB
                break;
            /**
             * Get the data from files_to_add and try to insert them into db.
             */
            this.startProcessNewFile();
            /** Going to sleep */
            if(canceled)
                break;
            System.out.println("Slepping...");
            try {
                TimeUnit.SECONDS.sleep(INTERVAL);
            } catch (InterruptedException ex) {
                /** 
                 * We get interrupt signal call for interupted sets it to false
                 * we *generaly* don't need it thought.
                 */
                Thread.interrupted();
            }
        }
        System.err.println("AnidbInserter ended");
    }

    /**
     * Starts file walker for selected path.
     */
    private void startFileWalker() {
        Path path = cm.getDownloadPath();
        if(path.toFile().isDirectory()) {
            FileWalker filewalker = new FileWalker(cm);
            try {
                Files.walkFileTree(path, filewalker);
            } catch (IOException e) {
                System.err.println("Error in calling walkFileTree.");
            }
        } else
            System.err.println("Wrong path given in config file.");
    }

    /**
     * Starts process new file for selected files in files_to_add db.
     */
    private void startProcessNewFile() {
        Map<String, List<Object>> map = DB.getDatabaseInfo(columns, database);
        int size = map.get("ed2k").size(); // randdom column just to get info how many rows we selected
        for(int i = 0; i<size; i++) {
            Path path = Paths.get((String)map.get("path").get(i));
            String hash = (String)map.get("ed2k").get(i);
            int fid = (int)map.get("fid").get(i);
            if(!path.toFile().isFile())  // if the file don't exist or for some reason is not a file we want to remove it from db.
                DB.deleteFromFilesToAdd(path);
            else if(fid > 0) // if we got fid we want only to move the file
                Episode.moveFile(path, fid, DB);
            else { // file exists is just not added to db.
                fid = DB.processNewFile(path, hash); // if bug it returns -1 we can't move file if it's -1
                if(fid != -1)    
                    if(Episode.moveFile(path, fid, DB))
                        DB.deleteFromFilesToAdd(hash); 
                    else // we can't move file (prodably is opened)
                        DB.insertFilesToAdd(path, hash, fid);
            }
        }
    }
    
    /**
     * Basic file walker.
     */
    private static class FileWalker implements FileVisitor<Path> {
        ConfigManager cm = new ConfigManager();
        private final Database db = AnidbJClientGUI.DB;
        private final ExecutorService exec;
        protected static boolean canceled = false;
        
        /**
         * Creates basic classes we will need and sets the number of executor services we will use.
         */
        FileWalker(ConfigManager cm) {
            this.cm = cm;
            int cores = Runtime.getRuntime().availableProcessors();
            cores = (int) Math.ceil(cores/2.0); // returns at least 1 
            this.exec = Executors.newFixedThreadPool(cores);
        }
        
        @Override
        public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
            if(canceled)
                return FileVisitResult.TERMINATE;
            if(db.checkFilesToAdd(path))
               return FileVisitResult.CONTINUE; // if file was alredy checked we move on
            if(Episode.checkFile(path, false)) {
                db.insertFilesToAdd(path); // insert into files_to_add so we know that file is already being counted.
                exec.execute(new Ed2kCounter(path.toFile()));
            }
            return FileVisitResult.CONTINUE;
        }
		
        @Override
        public FileVisitResult preVisitDirectory(Path dir, 
                BasicFileAttributes attrs) {
            //For visited directory
            //System.out.println("Found Directory: " + dir.getName(0));
            if(canceled)
                return FileVisitResult.TERMINATE;
            return FileVisitResult.CONTINUE;
        }
		
        @Override
        public FileVisitResult visitFileFailed(Path file,
                IOException exc) {
            exc.printStackTrace(System.err);
            if(canceled)
                return FileVisitResult.TERMINATE;
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
            if(canceled)
                return FileVisitResult.TERMINATE;
            if(dir == cm.getDownloadPath())
                return FileVisitResult.CONTINUE;
            try {
                Files.delete(dir);
            } catch (DirectoryNotEmptyException | AccessDeniedException e) {
            }
            return FileVisitResult.CONTINUE;
        }
    }
}
